什么是响应式
用 Excel 电子表格类比最直观:当 A2(价格)或 B2(数量)变化时,C2(总价)会自动重新计算。这种"数据变化 → 自动更新相关值"的机制就是响应式。
在 JavaScript 中,实现响应式需要解决三个问题:
- 存储副作用:当某个值变化时,需要知道执行哪些函数
- 追踪变化:当读取某个属性时,记录这个"谁在使用我"的关系
- 触发更新:当修改某个属性时,执行所有记录的副作用函数
副作用(Effect)的概念
let price = 10
let quantity = 2
let total = 0
// 副作用函数:依赖 price 和 quantity
function effect() {
total = price * quantity
}
effect() // 首次执行,total = 20
javascript
当 price 或 quantity 变化时,我们希望 effect 自动重新执行。
基础数据结构
单属性追踪:Set
使用 Set 存储某个属性的所有副作用(Set 自动去重):
const dep = new Set()
function track(key) {
dep.add(effect)
}
function trigger(key) {
dep.forEach(fn => fn())
}
javascript
多属性追踪:Map
一个对象有多个属性,每个属性各自有副作用集合:
const depsMap = new Map()
// key: 属性名(如 'price', 'quantity')
// value: Set<EffectFunction>
javascript
多对象追踪:WeakMap
多个对象各自的属性追踪,使用嵌套结构:
const targetMap = new WeakMap()
// key: 目标对象(如 product)
// value: Map<属性名, Set<EffectFunction>>
javascript
为什么用 WeakMap 而不是 Map?
| 特性 | Map | WeakMap |
|---|---|---|
| 键类型 | 任意类型 | 仅限对象 |
| 引用方式 | 强引用(阻止垃圾回收) | 弱引用(不阻止垃圾回收) |
| 可迭代性 | 可迭代(forEach、for...of) | 不可迭代 |
| 适用场景 | 通用键值存储 | 为对象附加临时信息 |
Vue 使用 WeakMap 是因为:当响应式对象不再被使用时,相关的依赖追踪数据可以被垃圾回收器自动清理,避免内存泄漏。
完整的 track 和 trigger 实现
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (activeEffect) {
dep.add(activeEffect)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
javascript
数据结构关系
WeakMap (targetMap)
│
├── key: product 对象
│ └── Map (depsMap)
│ ├── key: 'price' → Set([effect1, effect2])
│ └── key: 'quantity' → Set([effect1])
│
└── key: user 对象
└── Map (depsMap)
└── key: 'name' → Set([effect3])
text
这一节建立了 Vue 响应式系统的数据结构基础。下一节将介绍如何通过 Proxy 和 Reflect 自动触发 track 和 trigger。
↑